package astudy.networking.ftp;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

import org.apache.commons.io.IOUtils;

/**
 * basic ftp client.
 * now, it support:
 * connect, login, put/get, list, help
 * 
 * Features wanted in future:
 * 1. Error/Exception Control
 * 2. 断点续传
 * 3. Performance Tunning
 * 
 * @author wong tong
 *
 */
public class FtpClient {
	
	private String addr;
	private int port;
	private Socket client; 
	private ResponseHelper responseHelper;
	private BufferedWriter responseWriter;
	private File localDir;
	private PortThread portThread;
	
	public FtpClient(String addr) {
		create(addr, 21);
	}

	public FtpClient(String addr, int port) {
		create(addr, port);
	}
	
	private void create(String addr, int port) {
		
		this.addr = addr;
		this.port = port;
		String tmpDir = System.getProperty("java.io.tmpdir");
		this.localDir = new File(tmpDir);
	}
	
	public void connect() throws Exception {

		this.client = new Socket(this.addr, this.port);
		this.responseHelper = new ResponseHelper(new BufferedReader(new InputStreamReader(this.client.getInputStream())));
		this.responseWriter = new BufferedWriter(new OutputStreamWriter(this.client.getOutputStream()));
		System.out.println(this.responseHelper.getResponse());
	}
	
	public void login(String username, String password) 
	throws IOException {
		
		send(new Request("USER", username));
		System.out.println(this.responseHelper.getResponse());
		send(new Request("PASS", password));
		System.out.println(this.responseHelper.getResponse());
	}
	
	public void syst() throws IOException {

		send(new Request("SYST"));
		System.out.println(this.responseHelper.getResponse());
	}

	public void pwd() throws IOException {

		send(new Request("PWD"));
		System.out.println(this.responseHelper.getResponse());
	}
	
	public void quit() throws IOException {
		
		send(new Request("QUIT"));
		System.out.println(this.responseHelper.getResponse());
		this.responseHelper.close();
		this.responseWriter.close();
		this.client.close();
	}
	
	private void send(Request request) 
	throws IOException {
		
		responseWriter.write(request.toString());
		responseWriter.flush();
		System.out.println("\n[Request  CMD] " + request.toString().trim());
	}
	
	private String filename(String filepath) {
		
		String tmp = filepath.replaceAll("\\\\", "/");
		int index = tmp.lastIndexOf("/");
		if (index > -1) {
			return tmp.substring(index);			
		} else {
			return filepath;
		}
		
	}
	
	public File get(String filepath) throws Exception {

		String name = filename(filepath);
		File file = new File(this.localDir, name);
		if (file.exists()) {
			throw new IOException("Error: File '" + file.getAbsolutePath() + "'DOES exist!");
		} else {
			OutputStream fos = new FileOutputStream(file);
			this.port(new Request("RETR", filepath), fos);
			System.out.println(this.responseHelper.getResponse());
			System.out.println(this.responseHelper.getResponse());
			return file;
		}
	}
	
	public void put(String filepath) throws Exception {
		
		String name = filename(filepath);
		File file = new File(this.localDir, name);
		if (!file.exists()) {
			throw new IOException("Error: File '" + file.getAbsolutePath() + "' DOES NOT exist!");
		} else {
			this.port(new Request("STOR", filepath), new FileInputStream(file));
			System.out.println(this.responseHelper.getResponse());
			System.out.println(this.responseHelper.getResponse());
		}
	}

	/**
	 * 
	 * @param request
	 * @param os
	 * @throws Exception
	 */
	private void port(Request request, Object stream)
	throws Exception {

		//create a socket on a random port, 
		//tell ftp server by port command
		ServerSocket clientStreamSocket = new ServerSocket(0);
		int port = clientStreamSocket.getLocalPort();

		String addr = InetAddress.getLocalHost().getHostAddress().toString().replaceAll("\\.", ",");
		send(new Request("PORT", addr + "," + (port/256) + "," + (port%256)));
		System.out.println(this.responseHelper.getResponse());
		
		//start a new thread to get outputstream
		//then send request, such as LIST/RETR
		this.portThread = new PortThread(clientStreamSocket);
		if (stream instanceof OutputStream) {
			portThread.setOutputStream((OutputStream)stream);
		} else if (stream instanceof InputStream) {
			portThread.setInputStream((InputStream)stream);
		} else {
			throw new RuntimeException("Object stream should be an OuputStream or InputStream!");
		}
		
		portThread.start();
		send(request);
		//wait until portThread is over
		while (this.portThread.isAlive());
		this.portThread = null;
	}
	
	private class PortThread extends Thread {
		
		private ServerSocket clientStreamSocket;
		private OutputStream outputStream;
		private InputStream inputStream;
		private Boolean _ioFlag;

		public OutputStream getOutputStream() {
			return outputStream;
		}

		public void setOutputStream(OutputStream outputStream) {
			this.outputStream = outputStream;
			this._ioFlag = true;
		}

		public InputStream getInputStream() {
			return inputStream;
		}

		public void setInputStream(InputStream inputStream) {
			this.inputStream = inputStream;
			this._ioFlag = false;
		}

		public PortThread(ServerSocket clientStreamSocket) {
			this.clientStreamSocket = clientStreamSocket;
		}
		
		public void run() {
		
			Socket serverStreamSocket;
			try {
				serverStreamSocket = clientStreamSocket.accept();
				if (this._ioFlag && this.outputStream != null) {
					InputStream is = new BufferedInputStream(serverStreamSocket.getInputStream());
					IOUtils.copy(is, this.outputStream);
					serverStreamSocket.close();
				}
				if (!this._ioFlag && this.inputStream != null) {
					OutputStream os = new BufferedOutputStream(serverStreamSocket.getOutputStream());
					IOUtils.copy(this.inputStream, os);
					serverStreamSocket.close();
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	public String list(String dir) throws Exception {

		OutputStream os = new ByteArrayOutputStream();
		this.port(new Request("LIST", dir), os);
		//response
		System.out.println(this.responseHelper.getResponse());
		System.out.println(this.responseHelper.getResponse());

		//return
		return os.toString();
	}
	
	public Response help() throws IOException {
		
		send(new Request("HELP"));
		//parse response
		return this.responseHelper.getResponse();
	}
}
